﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DDD.Core {
    /// <summary>
    /// アニメーション クリップ クラス
    /// </summary>
    /// <remarks>
    /// アニメーション クリップは“歩く””走る”などの一連の動作をまとめたアニメーションの基本的な単位です。
    /// クリップは<see cref="AnimationPlayer"/> によって再生され、
    /// ターゲット オブジェクトのプロパティを元データである <see cref="AnimationTrack"/> を元に書き換えます。
    /// クリップは再生速度、ウェイト値を持ち、アニメーションの再生のされ方を制御可能です。
    /// <para>
    /// <h3>クリップの長さ</h3>
    /// TBD.
    /// </para>
    /// <para>
    /// <h3>ワールド時間とアニメーション ポジション</h3>
    /// シーン全体の時刻を ワールド時間 と呼びます。通常は msec 単位で計測されゲームのスタート時刻を0とする単調増加の整数値です。
    /// これに対してアニメーション クリップ内部で使用されるローカルな時刻を ローカル ポジション(*1) と呼び、
    /// ワールド時間 から以下の計算式を使って計算されます。
    /// <latex>\hspace{16} t_p = t_pref + s (t_w - twref)</latex>
    /// ここで、
    /// </para>
    /// <list type="bullet">
    ///   <item><latef expr="t_p"/> : 計算されたローカル ポジション</item>
    ///   <item><latef expr="t_w"/> : 与えられたワールド時間</item>
    ///   <item><latef expr="t_pref"/> : 同期ポイントのローカル ポジション</item>
    ///   <item><latef expr="t_wref"/> : 同期ポイントのワールド時間</item>
    ///   <item><latef expre="s"/> : 再生速度（ワールド時間1にたいするローカル ポジション）</item>
    /// </list>
    /// <para>
    /// 同期ポイントとは、あるワールド時間に再生されるポジションを指定した者です。
    /// <see cref="SetPlaybackPosition()"/> でセットれます。また <see cref="SetSpeed()"/> で再生速度を変化させた時に
    /// 新たに設定し直される事があります。
    /// アニメーションの値は求められたローカルポジションを元にキーレームデータを補完して求めます。
    /// 補完の方法の詳細については、TBD の解説を参照してください。
    /// </para>
    /// <para>
    /// <h3>再生速度の変更</h3>
    /// クリップの再生速度とはワールド時間に対するポジションの進み具合です。
    /// 初期値は<see cref="Speed"/> = 1 でワールド時間 1 に対してポジションが 1 進みます。
    /// 再生速度が 2 の場合はワールド時間 1 に対してポジションが 2 進みます。
    /// 結果としてアニメーションは2倍の速度で再生されます。再生速度は負の値を指定する事ができます。
    /// その場合はアニメーションが逆順で再生されます。
    /// 再生速度は <see cref="SetSpeed()"/> で変更可能です。
    /// <see cref="SetSpeed()"/> メソッドを呼ぶとアニメーションが不連続にならないように同期ポイントが変更される場合があります。
    /// 詳しくはメソッドの解説を参照してください。
    /// </para>
    /// <para>
    /// <div align="center"> <img src="AnimationClipDiagram.png"/> </div>
    /// <h3>アクティブ区間</h3>
    /// クリップには アクティブ区間 が設定されています。アクティブ区間はワールド時間で定義され、
    /// 該当クリップはこの区間でのみ有効です。区間外ではこのクリップは無視され、アニメーション対象のオブジェクトを書き換える事はありません。
    /// またアクティブ区間外に設定されているアニメーション イベントは発生しません。
    /// デフォルトではアクティブ区間は [-∞,∞] に設定されています。
    /// </para>
    /// <para>
    /// <h3>ウェイト</h3>
    /// クリップにはウェイト値が設定されています。初期値では <see cref="Weight"/> = 1.0 です。
    /// <see cref="AnimationPlayer"/> は、指定のワール時間の値をキーフレーム シーケンスから求めたのち、
    /// このウェイト値を乗算した値でターゲットを書き換えます。
    /// 複数のクリップが同じターゲットを書き換える場合、上記の値を単純に加算した値がセットされます。
    /// <latex>\hspace{16} P = \sum_i w_i P_i</latex> 
    /// ただしターゲットがクォータニオンの場合だけは最終的に求めた P をさらに正規化します。
    /// これはクリップが1つの場合だけでも行われます。
    /// <note type="implement">
    /// クォータニオンの正規化は現在実装されていない。
    /// かならず正規化するのが正しい仕様かどうか少し迷っている。
    /// </note>
    /// </para>
    /// </remarks>
    public class AnimationClip : Object {

        #region Field
        string name;
        List<AnimationTracker> trackers;
        ActiveInterval activeInterval;
        float speed;
        float weight;
        int refWorldTime;
        float refPosition;
        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 初期値は
        /// <list type="bullet">
        ///   <item><see cref="Weight"/> = 1</item>
        ///   <item><see cref="Speed"/> = 1</item>
        ///   <item><see cref="ActiveInterval"/> = [0,0)</item>
        ///   <item><see cref="Tracks"/> = なし</item>
        /// </list>
        /// です｡
        /// クリップの名前 <paramref name="name"/> は任意です｡エンジン側では使用しません｡
        /// </remarks>
        /// <param name="name">クリップの名前</param>
        public AnimationClip (string name) {
            this.name = name;
            this.trackers = new List<AnimationTracker> ();
            this.activeInterval = new ActiveInterval ();
            this.speed = 1;
            this.weight = 1;
            this.refWorldTime = 0;
            this.refPosition = 0;
        }
        #endregion

        #region Property
        /// <summary>
        /// 名前
        /// </summary>
        /// <remarks>
        /// コンストラクターでユーザーが指定した個のクリップの名前です。
        /// エンジン側では使用しません。
        /// </remarks>
        public string Name {
            get { return name; }
        }

        /// <summary>
        /// 再生速度
        /// </summary>
        /// <remarks>
        /// 現在の再生速度です。1.0が標準です。
        /// </remarks>
        public float Speed {
            get { return speed; }
        }

        /// <summary>
        /// ウェイト値
        /// </summary>
        /// <remarks>
        /// このクリップが重み付けに使われるウェイト値です。1.0が標準です。
        /// </remarks>
        public float Weight {
            get { return weight; }
        }

        /// <summary>
        /// アクティブ区間
        /// </summary>
        /// <remarks>
        /// この区間がアクティブな区間です。[0,0) はすべてのワールド時間でこのクリップは有効です。
        /// </remarks>
        /// <seealso cref="ActiveInterval"/>
        public ActiveInterval ActiveInterval {
            get { return activeInterval; }
        }

        /// <summary>
        /// アニメーション トラック数
        /// </summary>
        /// <remarks>
        /// このクリップが持つアニメーション トラックの合計です。
        /// <note type="implement">
        /// TrackCount ではなく TrackerCount の方が適切か？
        /// </note>
        /// </remarks>
        public int TrackCount {
            get { return trackers.Count (); }
        }

        /// <summary>
        /// すべてのトラックを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// このクリップが持つアニメーション トラックをすべて列挙します。
        /// 同一の <see cref="AnimationTrack"/> オブジェクトが含まれる場合があります。
        /// </remarks>
        /// <seealso cref="AnimationTrack"/>
        /// <seealso cref="Trackers"/>
        public IEnumerable<AnimationTrack> Tracks {
            get { return trackers.Select (x => x.SourceTrack); }
        }

        /// <summary>
        /// すべてのトラッカーを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// このクリップが持つトラッカー情報をすべて列挙します。
        /// <see cref="AnimationTracker"/> は（1）ターゲット オブジェクト、
        /// （2）書き換え対象のプロパティ名、（3）元データとなる <see cref="AnimationTrack"/>
        /// の3つの組みからなる構造体です。
        /// </remarks>
        /// <seealso cref="AnimationTracker"/>
        public IEnumerable<AnimationTracker> Trackers {
            get { return trackers; }
        }


        /// <summary>
        /// 再生時間
        /// </summary>
        /// <remarks>
        /// このクリップを1回再生した時にかかる時間（ワールド時間）です。
        /// <note type="important">
        /// 2013年3月以前はプレイバック ポジション単位で返していた。
        /// たぶん影響はないと思うが、しばらくここに注意書きを残しておく。  
        /// </note>
        /// </remarks>
        /// <exception cref="InvalidOperationException"><see cref="Speed"/> == 0</exception>
        public int Duration {
            get {
                if (Speed == 0) {
                    throw new InvalidOperationException ("Speed is 0");
                }
                return trackers.Max (x => x.SourceTrack.Duration);
            }
        }

        #endregion

        #region Method
        /// <summary>
        /// トラック(<see cref="AnimationTrack"/>)の設定
        /// </summary>
        /// <remarks>
        /// ターゲット <paramref name="target"/> のプロパティ <see cref="property"/> を書き換える
        /// アニメーション トラック <paramref name="track"/> を登録します。
        /// このクリップが再生されると指定のオブジェクトのプロパティはトラックの値によって書き換えられます。
        /// 以前の値は保存されません。
        /// アニメーションを付加できるプロパティの型には制限があります｡
        /// 詳しくは <see cref="AnimationPlayer"/> の解説、もしくは <see cref="IAnimatable{T}"/> の解説を参照してください｡
        /// <note type="implement">
        /// ここ何で式木を使わず直接文字列で指定してるの？？？
        /// 式木を使うのがセオリーな気がするが、何か理由があったかもしれない。
        /// 可能なら式木を使う方式に変更する。
        /// </note>
        /// <note type="implement">
        /// 現在の実装では何もチェックしていないので、ここで型チェックを厳密にやりたい。
        /// アニメーションは基本8種と、<see cref="IAnimatable{T}"/> を実装した構造体しか書き換えない。
        /// </note>
        /// <note type="implement">
        /// 配列のアニメーションに関しては仕様を決めていない。
        /// </note>
        /// </remarks>
        /// <param name="target">変更対象のオブジェクト</param>
        /// <param name="property">変更対象のプロパティ名</param>
        /// <param name="track">アニメーションの元になるトラック</param>
        /// <exception cref="ArgumentNullException"><paramref name="target"/> が <c>null</c></exception>
        /// <exception cref="ArgumentNullException"><paramref name="property"/> が <c>null</c> または空文字</exception>
        /// <exception cref="ArgumentNullException"><paramref name="track"/> が <c>null</c></exception>
        /// <seealso cref="AnimationTrack"/>
        /// <seealso cref="AnimationPlayer"/>
        public void AddTracker (Object target, string property, AnimationTrack track) {
            if (target == null) {
                throw new ArgumentNullException ("Target is null");
            }
            if (property == null || property == "") {
                throw new ArgumentNullException ("Property is null");
            }
            if (track == null) {
                throw new ArgumentNullException ("Track is null");
            }
            // もう少しチェックが必要
            // この段階で指定のオブジェクトにプロパティが存在していることをチェックする
            // プロパティの型とトラックに含まれるデータの互換性もチェックする

            this.trackers.Add (new AnimationTracker (target, property, track));
        }

        /// <summary>
        /// トラッカーの削除
        /// </summary>
        /// <remarks>
        /// 指定のトラック <paramref name="track"/> を使用する全てのトラッカーを削除します｡
        /// </remarks>
        /// <param name="track">アニメーション･トラック</param>
        public int RemoveTracker (AnimationTrack track) {
            return this.trackers.RemoveAll (x => x.SourceTrack == track);
        }

        /// <summary>
        /// トラッカーの取得
        /// </summary>
        /// <remarks>
        /// 指定のインデックス <paramref name="index"/> のトラッカーを取得します。
        /// </remarks>
        /// <param name="index">トラッカー インデックス</param>
        /// <exception cref="IndexOutOfRangeException"><paramref name="index"/> &lt; 0 または &gt;= <see cref="TrackCount"/></exception>
        /// <returns>トラッカー</returns>
        public AnimationTracker GetTracker (int index) {
            if (index < 0 || index >= TrackCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return trackers[index];
        }


        /// <summary>
        /// プレイバック ポジションの取得
        /// </summary>
        /// <remarks>
        /// 指定のワールド時間 <paramref name="worldTime"/> に、このクリップが再生するプレイパック ポジションを返します。
        /// プレイバック ポジションはループ モードに関わらず <see cref="Duration"/> で割らない値を返します。
        /// <note type="implement">
        /// このメソッドはワールド時間とポジションの線形マッピングを返す。
        /// ループ モードを考慮したポジションを返す実装もあり得るが、このメソッドはDDD内部で使いまくっているので
        /// 明確なメリットがなければ修正しない方が良い。
        /// </note>
        /// </remarks>
        /// <param name="worldTime">ワールド時間</param>
        /// <returns>プレイバック ポジション</returns>
        public float GetPosition (int worldTime) {
            return refPosition + speed * (worldTime - refWorldTime);
        }

        /// <summary>
        /// ワールド時間の取得
        /// </summary>
        /// <remarks>
        /// 指定のプレイバック ポジションが再生されるワールド時間を返します。
        /// <see cref="Speed"/> = 0 の時は計算できないので例外が発生します。
        /// <note type="implement">
        /// <paramref name="position"/> &gt; 0 で例外が発生するが、これは正しい仕様か？
        /// このメソッドはポジションとワールド時間の線形マッピングなので、ここで制限する必要性はないはず。
        /// なぜこういう実装にしたのか記憶がないので、とりあえずこのままにしているが修正すべきだろう。
        /// </note>
        /// </remarks>
        /// <param name="position">プレイバック ポジション</param>
        /// <exception cref="ArgumentException"><paramref name="position"/> &gt; 0</exception>
        /// <exception cref="InvalidOperationException"><see cref="Speed"/> == 0</exception>
        /// <returns>ワールド時間</returns>
        public int GetWorldTime (float position) {
            if (position < 0) {
                throw new ArgumentException ("Position is out of duration");
            }
            if (speed == 0) {
                throw new InvalidOperationException ("Speed is 0");
            }
            return (int)((position - refPosition) / speed + refWorldTime);
        }

        /// <summary>
        /// アクティブかどうかの判定
        /// </summary>
        /// <remarks>
        /// このクリップが指定のワールド時間 <see cref="worldTime"/> でアクティブかどうかを判定します。
        /// 指定のワールド時間がクリップの <see cref="ActiveInterval"/> の区間内か、常に有効に設定されているときアクティブです｡
        /// </remarks>
        /// <param name="worldTime">ワールド時間</param>
        /// <returns>アクティブなら<c>true</c>, そうでなければ <c>false</c></returns>
        /// <seealso cref="ActiveInterval"/>
        public bool IsActive (int worldTime) {
            var start = activeInterval.Start;
            var end = activeInterval.End;
            return (start == end) || (worldTime >= start && worldTime < end);
        }

        /// <summary>
        /// アクティブ区間の設定
        /// </summary>
        /// <remarks>
        /// このクリップのアクティブ区間を設定します(ワールド時間)｡
        /// 新しい区間は <paramref name="startTime"/> を含み <paramref name="endTime"/> を含みません｡
        /// <paramref name="startTime"/> == <paramref name="endTime"/> を指定すると常に有効になります(デフォルト)｡
        /// <note type="implement">
        /// ここで <paramref name="startTime"/> と <paramref name="endTime"/> を 0 以下に制限しているのは誤りだと思う。
        /// ワールド時間は原則 [-∞,∞] が有効範囲。
        /// </note>
        /// </remarks>
        /// <param name="startTime">アクティブ区間の開始時間(ワールド時間)</param>
        /// <param name="endTime">アクティブ区間の終了時間(ワールド時間)</param>
        /// <exception cref="ArgumentException"><paramref name="startTime"/> &lt; 0 または <paramref name="endTime"/> &lt; 0</exception>
        /// <exception cref="ArgumentException"><paramref name="startTime"/> &gt; <paramref name="endTime"/></exception>
        /// <seealso cref="ActiveInterval"/>
        public void SetActiveInterval (int startTime, int endTime) {
            if (startTime < 0 || endTime < 0) {
                throw new ArgumentException ("Start or end time is invalid");
            }
            if (startTime > endTime) {
                throw new ArgumentException ("Start and end time is invalid");
            }
            this.activeInterval = new ActiveInterval (startTime, endTime);
        }

        /// <summary>
        /// 同期ポイントの設定
        /// </summary>
        /// <remarks>
        /// 指定のワールド時間 <paramref name="worldTime"/> に指定のポジション <paramref name="position"/> が再生されるように
        /// 同期ポイントを変更します｡
        /// 通常 <paramref name="worldTime"/> には現在時刻を指定し、再生位置を指定のポジションに変更します｡
        /// 指定できるプレイバック ポジションは[0,∞)です｡
        /// </remarks>
        /// <param name="position">プレイバック ポジション</param>
        /// <param name="worldTime">ワールド時間</param>
        /// <exception cref="ArgumentException"><paramref name="position"/> &lt; 0</exception>
        public void SetPlaybackPosition (float position, int worldTime) {
            if (position < 0) {
                throw new ArgumentException ("Position is out of duration");
            }

            this.refPosition = position;
            this.refWorldTime = worldTime;
        }

        /// <summary>
        /// ウェイト値の変更
        /// </summary>
        /// <remarks>
        /// このクリップの重みです｡デフォルトでは 1.0 に設定されています｡
        /// <see cref="AnimationTrack"/> をサンプリングした値は、このウェイ値をかけた後、
        /// ターゲットのプロパティに書き込まれます。
        /// 複数のクリップが同一のプロパティを書き換える場合は単に加算されます｡
        /// 詳しくは <see cref="AnimationClip"/> の解説を参照してください。
        /// <note>
        /// 何となくウェイト値を0以上に制限しているが、理由があっての事ではない。
        /// </note>
        /// </remarks>
        /// <param name="weight">ウェイト値</param>
        /// <exception cref="ArgumentException"><paramref name="weight"/> &lt; 0</exception>
        public void SetWeight (float weight) {
            if (weight < 0) {
                throw new ArgumentException ("Weight is invalid");
            }
            this.weight = weight;
        }

        /// <summary>
        /// 再生速度の変更
        /// </summary>
        /// <remarks>
        /// このクリップの再生速度を変更します。アニメーションが不連続にならないように
        /// 同期ポイント <paramref name="refWorldTime"/> を指定して変更します｡
        /// 通常この同期ポイントは現在時刻です｡
        /// <note type="implement">
        /// ここで<see cref="Speed"/> &lt 0 で例外を発生しているのは明らかに実装ミス。
        /// 再生速度は負の値も許される（でないと逆再生できないからね～）
        /// </note>
        /// </remarks>
        /// <param name="speed">新しい再生速度</param>
        /// <param name="refWorldTime">同期ポイント(ワールド時間)</param>
        /// <exception cref="ArgumentException"><see cref="Speed"/> &lt; 0</exception>
        public void SetSpeed (float speed, int refWorldTime) {
            if (speed < 0) {
                throw new ArgumentException ("Speed is invalid");
            }
            this.refPosition = GetPosition (refWorldTime);
            this.refWorldTime = refWorldTime;
            this.speed = speed;
        }

        /// <inheritdoc/>
        public override string ToString () {
            return string.Format ("{0}", Name);
        }
        #endregion
    }


}
